﻿using System.Collections.Generic;
using System.Data.Linq;
using System.Data.SqlTypes;
using System.Linq;
using Microsoft.SqlServer.Types;
using log4net;

namespace Storage.EntityModels
{
    using Exceptions;

    #region Stage

    /// <summary>
    /// This class represents the Stages table into entities model.
    /// </summary>
    public partial class Stage : IPersistable
    {
        #region Private fields: _path, _viaPoints, _logger

        private SqlGeography _path = new SqlGeography();
        private SqlGeography _viaPoints;

#if LOG
        private static readonly ILog _logger = LogManager.GetLogger(typeof(Stage));
#endif

        #endregion

        #region Public properties: Distance, Path, ViaPoints, Origin, Destination

        /// <summary>
        /// Total distance in km.
        /// </summary>
        public double Distance
        {
            get
            {
                SqlDouble result = 0;

                for (int i = 1; i < Path.STNumPoints(); i++)
                {
                    SqlGeography point = Path.STPointN(i);
                    result += point.STDistance(Path.STPointN(i + 1));
                }

                return result.Value / 1000;
            }
        }

        /// <summary>
        /// Gets the Path of a Stage.
        /// It consists of a sequence of points.
        /// </summary>
        public SqlGeography Path { get { return _path; } }

        /// <summary>
        /// Gets the Viapoints of a Stage.
        /// It consists of a sequence of points. 
        /// </summary>
        public SqlGeography ViaPoints { get { return _viaPoints; } }

        /// <summary>
        /// Gets and Sets the origin Place.
        /// </summary>
        public Place Origin
        {
            get
            {
                return Origin_;
            }
            set
            {
                Origin_ = getOrCreatePlace(value);
            }
        }

        /// <summary>
        /// Gets and Sets the destination Place.
        /// </summary>
        public Place Destination
        {
            get
            {
                return Destination_;
            }
            set
            {
                Destination_ = getOrCreatePlace(value);
            }
        }

        /// <summary>
        /// Gets an existing Place or create a new one if it isn't.
        /// </summary>
        /// <param name="value">The Place to check for the existence.</param>
        private static Place getOrCreatePlace(Place value)
        {
            // Try to get a stored instance of the Place.
            double lat = (double)value.MapPoint.Lat;
            double lng = (double)value.MapPoint.Long;

            Place result = StorageManager.Instance.GetPlaceByLatLong(lat, lng);

            if (result == null) // The Place wasn't found ... 
            {
                // ... however the Place could be in the ChangeSet as a insert.
                result = value;

                var changeSet = StorageManager.Instance.Entities.GetChangeSet();
                var new_places = from added in changeSet.Inserts
                                 where added.GetType() == typeof(Place)
                                 select (Place)added;

                foreach (var place in new_places)
                {
                    if (place.MapPoint.Lat == lat && place.MapPoint.Long == lng)
                    {
                        result = place;
                        break;
                    }
                }

            } // if (result == null)

            return result;
        }

        #endregion

        #region Public methods: GetPathPoints, GetViaPoints, SetPath, SetViaPoints, AddDrivingInfo

        /// <summary>
        /// Returns path of Stage as enumeration.
        /// </summary>
        public IEnumerable<SqlGeography> GetPath()
        {
            for (int i = 1; i <= Path.STNumPoints(); i++)
            {
                yield return Path.STPointN(i);
            }
        }

        /// <summary>
        /// Returns viapoints of Stage as enumeration.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<SqlGeography> GetViaPoints()
        {
            if (ViaPoints != null)
            {
                for (int i = 1; i <= ViaPoints.STNumPoints(); i++)
                {
                    yield return ViaPoints.STPointN(i);
                }
            }
        }

        /// <summary>
        /// Sets the path of a Stage from a string containing path data.
        /// </summary>
        /// <remarks>The regular expression is "x1 y1, x2 y2 [,xn yn]."</remarks>
        /// <example>string.Format("{0} {1}, {2} {3}")</example>
        /// <param name="path">The path as a string.</param>
        /// <param name="isTrain">A flag that indicates if the stage's vehicle is train</param>
        public void SetPath(string path, bool isTrain)
        {
            if (isTrain)
            {
                string completePath = string.Format("LINESTRING({0})", path);
                _path = SqlGeography.STLineFromText(new SqlChars(new SqlString(completePath)), 4326);
            }
            else
            {
                string completePath = string.Format("MULTIPOINT({0})", path);
                _path = SqlGeography.STMPointFromText(new SqlChars(new SqlString(completePath)), 4326);
            }

            StorageManager.Instance.Entities.AddToCustomUpdates(this);
        }

        /// <summary>
        /// Sets the viapoints of a Stage from a string containing viapoints data.
        /// </summary>
        /// <remarks>The regular expression is ((n n), (n n)[,(n n)])</remarks>
        /// <example>string.Format("({0} {1}), ({2} {3})</example>
        /// <param name="viaPoints">The viapoints as a string.</param>
        public void SetViaPoints(string viaPoints)
        {
            string completeViaPoints = string.Format("MULTIPOINT({0})", viaPoints);
            _viaPoints = SqlGeography.STMPointFromText(new SqlChars(new SqlString(completeViaPoints)), 4326);
            
            StorageManager.Instance.Entities.AddToCustomUpdates(this);
        }

        /// <summary>
        /// Adds a DrivingInfo to current Stage.
        /// </summary>
        /// <param name="mapPoint">The point the driving info is related to.</param>
        /// <param name="info">The info.</param>
        public void AddDrivingInfo(SqlGeography mapPoint, string info)
        {
#if LOG
            _logger.Debug(string.Format("AddDrivingInfo"));
#endif
            DrivingInfos.Add(new DrivingInfo
            {
                MapPoint = mapPoint, // No custom update! Filtered by AddToCustomUpdates itself.
                Info = info,
                Stage = this,
                StageId = this.Id,
                InfoOrdinal = (short)(DrivingInfos.Max(d => d.InfoOrdinal) + 1)
            });
        }

        /// <summary>
        /// Adds a DrivingInfo to current Stage.
        /// </summary>
        /// <param name="mapPoint">The point the driving info is related to.</param>
        /// <param name="info">The info.</param>
        public void AddDrivingInfo(string mapPoint, string info)
        {
            mapPoint = string.Format("POINT({0})", mapPoint);
            AddDrivingInfo(SqlGeography.STPointFromText(new SqlChars(new SqlString(mapPoint)), 4326), info);
        }

        #endregion

        #region Partial methods: OnValidate, OnLoaded

        partial void OnValidate(ChangeAction action)
        {
#if DEBUG
            if (Ordinal <= 0) 
                throw new FieldNotValidException("Ordinal in Stage must be at least 1.");

            //if (action == ChangeAction.Insert)
            //{
            //    var dc = StorageManager.Instance.Entities;
            //    if (dc.Stages.Any(s => s.TripId == TripId && s.Ordinal == Ordinal))
            //    {
            //        throw new FieldNotValidException(string.Format("A Stage with same TripId and"
            //                                        + " Ordinal already exists. StageId: {0}, TripId: {1}",
            //                                        Id, TripId));
            //    }
            //}
            
            if (Users_VehicleDetails == null)
                throw new FieldNotValidException("The Stage must have a vechicle.");

            if (Users_VehicleDetails.VehicleDetails == null)
                throw new FieldNotValidException("The Stage doesn't have a valid vehicle. "
                    + " Set Users_VehicleDetails.VehicleDetails to something != null.");

            if (Users_VehicleDetails.User == null)
                throw new FieldNotValidException("The Stage is using a vehicle that belongs to a User that " + 
                    "is not bookein anymore.");

            if (Origin.MapPoint.ToString() != _path.STStartPoint().ToString() ||
                Destination.MapPoint.ToString() != _path.STEndPoint().ToString())
                throw new FieldNotValidException("Origin and Destination in Stage must refer to the first"
                    + " and to the last point in Path.");

            // Check if each stage with "ordinal > 1", has his first place equal to 
            // the last place of the previous stage.
            if (Ordinal > 1)
            {
                var changeSet = StorageManager.Instance.Entities.GetChangeSet();
                var orderedStage = changeSet.Inserts.Union(changeSet.Updates)
                                        .OfType<Stage>()
                                        .OrderBy(stage => stage.Ordinal);

                for (short ordinal = 1; ordinal < orderedStage.Count(); ordinal++)
                {
                    Stage stage = orderedStage.ElementAt(ordinal);

                    if (stage.OriginPlaceId != orderedStage.ElementAt(ordinal - 1).DestinationPlaceId)
                        throw new FieldNotValidException(string.Format("Stage sequential error: Stage with tripId = {0} and Ordinal" +
                            " = {1} must have OriginPlaceId = {2}.", stage.TripId, stage.Ordinal, 
                            orderedStage.ElementAt(ordinal - 1).DestinationPlaceId));
                }
            }
#endif
        }

        partial void OnLoaded()
        {
            TripLoaDataContext db = StorageManager.Instance.Entities;
            var q = db.Stage_selectGeoRefDataByPk(Id); 
            var first = q.First();

            if (first.Path.StartsWith("LINESTRING"))
            {
                _path = SqlGeography.STLineFromText(new SqlChars(first.Path), 4326);
            }
            else
            {
                _path = SqlGeography.STMPointFromText(new SqlChars(first.Path), 4326);
            }
            
            _viaPoints = SqlGeography.STMPointFromText(new SqlChars(first.ViaPoints), 4326);
        }

        partial void OnCreated()
        {
            PropertyChanged += (o, e) =>
            {
                if (e.PropertyName == "Origin")
                {
                    // TODO
                }
                else if (e.PropertyName == "Destination")
                {
                    // TODO
                }
            };
        }

        #endregion

        #region IPersistable Members

        /// <summary>
        /// This function insert a Stage entity into Stages table. 
        /// </summary>
        /// <param name="db"></param>
        public void OnInsert(BaseTripLoaDataContext db)
        {
            int? stageId = 0;
            Binary viaPointsToByteArray = null;

            if (ViaPoints != null  && !ViaPoints.IsNull)
                viaPointsToByteArray = ViaPoints.STAsBinary().Buffer;

            db.Stage_insertAsBinary(ref stageId, TripId, Ordinal, OriginPlaceId, DestinationPlaceId, 
                DriverId, Users_VehicleDetailsId, ReservedSeats, BusySeats, EstimatedTime, EstimatedBudget,
                Path.STAsBinary().Buffer, viaPointsToByteArray);

            Id = stageId.Value;
        }

        /// <summary>
        /// This function update a Stage into Stages table. 
        /// </summary>
        /// <param name="db"></param>
        public void OnUpdate(BaseTripLoaDataContext db)
        {
            string viaPointsAsString = null;

            if (ViaPoints != null && !ViaPoints.IsNull)
                viaPointsAsString = ViaPoints.ToString();
            
            db.Stage_updateAsString(Id, TripId, Ordinal, OriginPlaceId, DestinationPlaceId, DriverId, 
                Users_VehicleDetails.Id, ReservedSeats, BusySeats, EstimatedTime, EstimatedBudget, Path.ToString(),
                viaPointsAsString);
        }

        #endregion
    }

    #endregion
}
